PromQL 查询语言
前面两篇把 Prometheus 架起来、也讲清了 数据模型与抓取原理,但日常用 Prometheus,八成时间其实花在写查询上:画图、配告警、排障看数,全是 PromQL。这篇笔记按「数据长什么样 → 怎么选序列 → 必会函数 → 实战菜谱」的顺序,把日常要用到的写法过一遍。示例都可以直接粘到表达式浏览器(Web 界面的 Graph 页,http://localhost:9090/graph)里跑。环境口径:Prometheus 2.5x / 3.x,node_exporter 1.x。
数据模型
指标名 + 标签集 = 一条时间序列
Prometheus 里存的所有数据都是时间序列。一条序列由两部分唯一确定:
- 指标名(metric name):说明这是什么指标,比如
http_requests_total、node_cpu_seconds_total - 标签集(labels):一组 key=value 对,描述这条序列的维度,比如
instance="10.0.0.1:9100"、job="node"
同一个指标名、标签只要有一个不同,就是另一条序列。比如 http_requests_total{job="api", handler="/users"} 和 http_requests_total{job="api", handler="/orders"} 是两条独立的序列,各自计数、互不影响。
一个样本(sample)就是一个「时间戳 + 值」的二元组:毫秒精度的 Unix 时间戳,加一个 float64 的值。序列就是样本按时间排起来的流。查询的本质,就是从这些序列里选一批、算一算。
查询结果的类型
在表达式浏览器里敲任何查询,结果只有四种类型:
| 类型 | 例子 | 说明 |
|---|---|---|
| 瞬时向量 instant vector | http_requests_total | 一组序列,每条只有「查询时刻」的一个样本 |
| 区间向量 range vector | http_requests_total[5m] | 一组序列,每条带最近 5 分钟的一串样本 |
| 标量 scalar | 1024、单位换算里的常数 | 单个浮点数,没有标签 |
| 字符串 string | 基本用不到 | 单个字符串 |
记住两条硬规则:
- 告警规则和画图用的是瞬时向量;区间向量不能直接拿去告警,它只是函数的输入
rate()、increase()这类「看一段时间变化」的函数只吃区间向量,所以括号里永远带着[5m]这样的区间
查询时刻通常不会正好踩到样本。Prometheus 会从查询时刻向前回看最多 5 分钟(lookback delta),取每条序列的最后一个样本。所以 target 刚下线时它的值还会「残留」最多 5 分钟——这不是 bug,是设计如此。
选择器
标签匹配的四种写法
选中一批序列靠标签匹配器,一共四种:
| 匹配器 | 含义 | 例子 |
|---|---|---|
= | 等值 | up{job="prometheus"} |
!= | 不等 | http_requests_total{status!="200"} |
=~ | 正则匹配 | http_requests_total{status=~"5.."} |
!~ | 正则不匹配 | node_filesystem_size_bytes{fstype!~"tmpfs|overlay"} |
两个要点:
- 正则是完全锚定的:
status=~"5"只匹配值恰好是"5"的序列,想匹配 5xx 要写5..。正则引擎是 RE2,不支持反向引用 - 标签值为空等于没有这个标签:
env=""选中的是「没有 env 标签」的序列,配合!=/=~排查标签缺失时很有用
只写指标名 up 是选这个指标的全部序列;理论上也可以只写匹配器不带指标名,但 {job=~".+"} 这种写法等于全表扫描,生产上别用。
区间选择
选择器后面加 [时长],瞬时向量就变成区间向量:
# 最近 5 分钟的所有样本
http_requests_total[5m]
# 带标签过滤的区间向量
node_cpu_seconds_total{mode="idle", cpu="0"}[10m]
时长单位:s 秒、m 分、h 时、d 天、w 周、y 年,可以组合成 [1h30m]。区间向量基本只作为函数输入;直接在表达式浏览器跑也能返回,但那是一堆原始样本点,没什么可读性。
offset 偏移
offset 把查询的参考时刻整体往回挪,用来做历史同期对比:
# 现在的 QPS
sum(rate(http_requests_total[5m]))
# 一小时前同时段的 QPS
sum(rate(http_requests_total[5m] offset 1h))
# 差值:比一小时前涨了多少
sum(rate(http_requests_total[5m]))
-
sum(rate(http_requests_total[5m] offset 1h))
排障时常用「现在 vs 昨天 / 上周同时段」确认问题是不是周期性的。另外还有个 @ 修饰符能把查询钉在某个绝对时间戳上,日常用得少,知道有这东西就行。
必会函数
rate、increase、irate:Counter 三件套
Counter 类型(xxx_total 结尾)的值只增不减,原始值本身没意义,必须算「涨得多快」。三个函数的区别:
| 函数 | 算法 | 适用场景 |
|---|---|---|
rate() | 区间内首尾差 ÷ 时长,算每秒平均增速 | 画图、告警,绝大多数场景 |
increase() | rate() 结果 × 区间秒数,算这段时间涨了多少总量 | 「最近 1 小时处理了多少请求」 |
irate() | 只用区间内最后两个样本算瞬时增速 | 排障看瞬时尖刺 |
# 每台机器每秒的 HTTP 请求数
rate(http_requests_total[5m])
# 最近 1 小时总共处理了多少请求
increase(http_requests_total[1h])
经验法则一句话:默认用 rate;要看总量用 increase;irate 只在排障看尖峰时用。告警别用 irate——它太灵敏,一个抖动就触发。
sum、avg、max、min 与 by / without 分组
聚合运算符把多条序列合并成少数几条,by 和 without 控制按什么维度分:
by (标签...):只保留列出的标签,按它们分组without (标签...):去掉列出的标签,按剩下的分组
# 全部实例的总 QPS(合并成一条线)
sum(rate(http_requests_total[5m]))
# 按实例分开看 QPS(每台一条线)
sum by (instance) (rate(http_requests_total[5m]))
# 按 job 看,忽略实例和接口维度
sum without (instance, handler) (rate(http_requests_total[5m]))
avg / max / min / count 用法完全一样。顺序要记死:rate 写在 sum 里面。直觉上「先加总再求速率」的 rate(sum(...)) 直接是语法错误——而且就算能写,先加总在 Counter 重启归零时也会算错。
topk / bottomk
从一堆序列里挑出最大或最小的 k 条:
# CPU 使用率最高的 5 个实例
topk(5, 100 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
# 剩余磁盘最少的 3 个挂载点
bottomk(3, node_filesystem_avail_bytes)
k 是常量,序列不足 k 条时返回全部。画图时 topk 很常用——几百条序列直接糊在一张图上没法看,只画头部几条。
histogram_quantile:算 P95 / P99 延迟
直方图指标(xxx_bucket 结尾、带 le 标签)的 quantile 计算是 PromQL 里最绕的一段,拆开看:
histogram_quantile(0.99,
sum by (le) (rate(http_request_duration_seconds_bucket{job="api"}[5m])))
从内到外逐段解释:
http_request_duration_seconds_bucket{job="api"}:选出一组桶序列,每条带一个le(less than or equal)标签,le="0.1"表示「耗时不超过 0.1 秒的请求累计数」。桶是累计的:le="0.5"的计数包含了所有更快桶里的请求rate(...[5m]):算每个桶每秒新增多少请求,把只增不减的累计数变成速率sum by (le) (...):把同一le值下不同实例、不同接口的桶合并,只留 le 这一个维度——这步必须有,少了它 quantile 没法算histogram_quantile(0.99, ...):在相邻桶之间做线性插值,估算出「99% 的请求比这个值快」
估算值只能落在桶边界附近。如果最粗的桶是 le="10",实际 P99 再高也只报 10 秒。想要准的 P99,埋点时桶的划分要覆盖你关心的延迟区间。
配套的 _sum 和 _count 序列还能算平均延迟:
# 平均延迟 = 总耗时 / 请求总数
sum(rate(http_request_duration_seconds_sum[5m]))
/
sum(rate(http_request_duration_seconds_count[5m]))
absent:检测「没有数据」
告警里最怕的不是值异常,而是序列整个没了——指标消失往往说明 exporter 挂了或者目标掉了,这本身就是故障。absent() 干的就是这个:
# 这个 job 一条序列都没有时返回 1,有数据时返回空
absent(up{job="api"})
# 检测某个具体指标是否消失
absent(http_requests_total{job="api"})
absent 的语义和「值为 0」完全不同:没数据时返回一条值为 1 的序列,有数据时返回空。判断「target 是不是全挂了」用 absent(up{job="xxx"}) 比拿 up 的值去比 0 可靠——后者在序列消失时什么都报不出来。
运算符
算术与比较
算术运算符 + - * / % ^ 没太多好说的,两边可以是标量或向量。常用的「标量换算向量」:
# 字节换算成 GiB
node_memory_MemAvailable_bytes / 1024^3
# 增速换算成百分比
rate(node_cpu_seconds_total{mode="idle"}[5m]) * 100
比较运算符 == != > < >= <= 的默认行为是过滤:只保留满足条件的序列,值原样保留:
# 只返回磁盘剩余不足 100 GiB 的挂载点,值还是字节数
node_filesystem_avail_bytes < 100 * 1024^3
加个 bool 修饰符,行为变成判定:每条序列都保留,满足条件值为 1、不满足为 0:
# 每条序列返回 0 或 1,适合统计有多少台不达标
node_filesystem_avail_bytes < bool 100 * 1024^3
# 数一下有几台磁盘告急
count(node_filesystem_avail_bytes < bool 100 * 1024^3)
告警用过滤语义(报出来的值是原始值,可读性好);统计「有几台超标」这类需求用 bool。
逻辑运算符
三个集合运算,作用于向量之间:
| 运算符 | 语义 | 典型用途 |
|---|---|---|
and | 交集:两边都有才保留 | 多条件同时成立才告警 |
or | 并集:任一边有就保留 | 合并多条告警表达式 |
unless | 差集:左边有、右边没有才保留 | 排除已知问题 |
# 磁盘空间不足 AND inode 也不足,才告警
(node_filesystem_avail_bytes / node_filesystem_size_bytes < 0.1)
and
(node_filesystem_files_free / node_filesystem_files < 0.1)
向量与向量运算时默认按标签完全匹配来对齐序列;两边标签集对不上时要用 on() / ignoring() 显式指定匹配键,属于进阶写法,用到再查文档。
实战菜谱
每道菜一行查询加一句话场景,直接粘到表达式浏览器就能跑。
1. CPU 使用率(%)
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
1 减去 idle 占比就是 busy 占比;avg by (instance) 把每台机器的所有核合成一条线。
2. 内存使用率(%)
(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100
用 MemAvailable 而不是 MemFree:前者把可回收的 cache/buffer 算作可用,才是真实可用内存。
3. 磁盘 4 小时后会不会满
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4 * 3600)
按最近 6 小时的线性趋势外推,预测 4 小时后的剩余字节数;结果小于 0 就是「撑不过 4 小时」,告警表达式常配 < 0。
4. 接口 QPS
sum by (handler) (rate(http_requests_total[5m]))
按接口分开看每秒请求数;想看总量就把 by 子句去掉。
5. 错误率(5xx 占比,%)
sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) * 100
分子分母用同一个区间长度,比值才有意义;流量太小时比例会乱跳,配告警要留神。
6. P99 延迟(秒)
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
中间的 sum by (le) 不能省——直接对原始桶序列算 quantile 会按实例、接口各自分开插值,结果没有意义。
7. 最近 5 分钟有变化的序列
changes(http_requests_total[5m]) > 0
changes() 返回区间内值变了几次,大于 0 说明这条序列还活着;排查「哪些指标停了」时反着用,找等于 0 的。
排错与优化
查询慢、浏览器卡死、Prometheus OOM,九成是基数(cardinality)问题——标签组合太多。定位手段:
- Web 界面 Status → TSDB Stats 页面能看 Head 里的序列总数,以及按指标名、按标签的 Top 10 统计,一眼看出哪个指标爆炸
- 表达式浏览器的 Table 标签页会显示本次查询加载了多少条序列,上了万级就该警惕
- 常见凶手:把 user_id、session_id、带参数的 URL 路径做成标签值,一个指标轻松上百万条序列
写法上的几个原则:
- 先过滤再聚合:
sum(rate(http_requests_total{job="api"}[5m]))比不带过滤条件的版本少算一堆无关序列 - 别选全量:不带指标名、只写匹配器的查询等于全表扫描,生产面板里不要出现
- rate 区间别贪小:
[1m]配 30 秒抓取间隔,区间内可能只有两三个样本,算出来全是毛刺还白加重计算 - 重查询下沉成 Recording Rules:面板和告警反复用到的复杂表达式,预计算成一个新指标,查询时直接取
# recording rule 示例:把每台实例的 CPU 使用率预计算出来
groups:
- name: node_rules
interval: 30s
rules:
- record: instance:node_cpu_utilization:ratio
expr: 1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))
之后查询直接写 instance:node_cpu_utilization:ratio,不用每次都扫原始序列,面板加载和告警评估都快得多。
常见坑
按踩坑频率排:
- rate 区间太短全是毛刺。rate 只用区间内首尾样本做差分,区间太短、样本太少,一点抖动就被放大成尖峰。经验值:区间至少 4 倍抓取间隔——默认 15 秒抓取就
[1m]起步,画图告警常用[5m]。 - increase 算出小数不是 bug。
increase(http_requests_total[5m])返回 12.7 很正常:它是 rate 乘时长算出来的外推值,不是逐样本加出来的整数。想要更贴近真实计数就拉长区间,或者接受这是估计值。 - Counter 重启归零不用特殊处理。exporter 重启后计数器从 0 重新来,rate/increase 内置了 counter reset 检测:发现值变小就当「归零后重新计数」处理,不会算出负数尖峰。进程重启了,查询照常写就行。
- 值的时间戳和想象的不一样。瞬时向量是「查询时刻向前回看最多 5 分钟取最后一个样本」,所以 target 下线后值会残留最多 5 分钟,图上某个点的实际采样时间也可能比显示时刻早。对时间线排障时心里要有这根弦。
- 除法或运算结果是空的,不是报错。
a / b里 b 在该时刻没有序列,结果就是空,图上直接断线。常见原因是两边标签集对不上、或者一边的过滤条件把序列筛没了。空结果在 PromQL 里不是错误——悄悄没数据最坑人,排查时把表达式拆成两半分别跑一遍。
一个多层嵌套的表达式算不出结果时,从最内层开始逐层粘到浏览器里跑,确认每一层选出来的序列和值都对,再往外包。比盯着整条表达式干看快得多。
小结
- 数据模型一句话:指标名 + 标签集确定一条序列,样本是「时间戳 + 值」;查询结果记两种向量——瞬时向量用于告警画图,区间向量只做函数输入
- 选择器四种匹配
=、!=、=~、!~,正则完全锚定;[5m]转区间向量,offset看历史同期 - Counter 必配 rate:画图告警用
rate(),看总量用increase(),irate()只在排障看尖峰时用 - 聚合顺序记死:
sum(rate(...)),rate 永远在内层;直方图算延迟必须有sum by (le) absent()管「指标消失」;比较运算符默认过滤序列,加bool返回 0/1- 慢查询先怀疑基数:TSDB Stats 看序列数,先过滤再聚合,重查询下沉 recording rules
- rate 区间至少 4 倍抓取间隔,increase 出小数正常,Counter 归零被自动处理
下一篇讲告警:Alertmanager 的路由、分组与静默,以及怎么把这篇写的表达式变成不吵人的告警规则。